/**
 * Project Name:jbase
 * File Name:BeanContext.java
 * Package Name:jbase.project.system.core
 * Date:2017年4月21日下午11:30:52
 * Copyright (c) 2017, chenzhou1025@126.com All Rights Reserved.
 *
*/

package jbase.project.system.core.ioc;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import jbase.project.system.core.annotations.ioc.Auto;
import jbase.project.system.core.annotations.ioc.Init;
import jbase.project.system.core.annotations.ioc.Instance;
import jbase.project.system.core.annotations.ioc.Property;
import jbase.project.system.core.annotations.ioc.Reference;
import jbase.project.system.core.annotations.ioc.Value;
import jbase.project.system.core.support.LocalService;
import jbase.project.system.exception.SysException;

public class BaseContext {

	private static final Log log = LogFactory.getLog(BaseContext.class);

	private List<BaseInstance> beans = new ArrayList<>();
	private Map<String, Object> properties = new HashMap<>();

	public BaseContext() {
	}

	public BaseContext(Map<String, Object> properties) {
		this.properties.putAll(properties);
	}

	public void setProperty(String name, Object value) {
		this.properties.put(name, value);
	}

	public Object getProperty(String name) {
		return this.properties.get(name);
	}

	public void addProperties(Map<String, Object> properties) {
		this.properties.putAll(properties);
	}

	public Map<String, Object> getProperties() {
		return properties;
	}

	public <T> T getBeanByType(String className) throws SysException {
		try {
			return getBeanByType(Class.forName(className));
		} catch (Exception e) {
			throw new SysException(e, "cannot instance the bean of class " + className);
		}
	}

	public void initBeanDefine(String... xml) {
		
	}

	@SuppressWarnings("unchecked")
	public <T> T get(String id) throws SysException {
		log.debug("get method invoked, id = " + id);
		Iterator<BaseInstance> bit = beans.iterator();
		while (bit.hasNext()) {
			BaseInstance bi = bit.next();
			log.debug("bi scan, current = " + bi.toString());
			if (bi.getId().equals(id)) {
				if (bi.getIsCreated()) {
					return (T) bi.getInstance();
				}
			}
		}
		throw new SysException("cannot find the object " + id);
	}

	@SuppressWarnings("unchecked")
	public <T> T getBeanByType(Class<?> beanType) throws SysException {
		Iterator<BaseInstance> bit = beans.iterator();
		List<T> finds = new ArrayList<>();
		while (bit.hasNext()) {
			BaseInstance bi = bit.next();
			if (bi.getType() == beanType) {
				if (bi.getIsCreated()) {
					finds.add((T) bi.getInstance());
				}
			}
		}
		if (finds.size() == 1) {
			return finds.get(0);
		} else if (finds.size() >= 2) {
			throw new SysException(
					"there are " + finds.size() + " be found, and sys cannot decsion what bean to return!");
		} else if (finds.size() == 0) {
			String value = LocalService.uuid();
			String name = beanType.getSimpleName();
			String desc = LocalService.listClassInfo(beanType);
			log.debug("default properties: " + "id=" + value + ";name=" + name + ";desc=" + desc);
			Annotation[] typeAnnotations = beanType.getAnnotations();
			for (Annotation annotation : typeAnnotations) {
				log.debug("find annotation:" + annotation.annotationType().getName());
				if (annotation.annotationType() == Instance.class) {
					Instance i = (Instance) annotation;
					if (value != null && !value.trim().equals("")) {
						value = i.value();
						name = i.name();
						desc = i.desc();
					}
				}
			}
			log.debug("create-prev properties: " + "id=" + value + ";name=" + name + ";desc=" + desc);
			return create(beanType, value, name, desc);
		} else {
			throw new SysException("sys exception");
		}
	}

	@SuppressWarnings("unchecked")
	public <T> T newInstance(Class<?> beanType) throws SysException {
		Constructor<?>[] cs = beanType.getConstructors();
		Constructor<?>[] dcs = beanType.getDeclaredConstructors();
		List<Constructor<?>> cl = new ArrayList<>();
		for (Constructor<?> c : cs) {
			cl.add(c);
		}
		for (Constructor<?> c : dcs) {
			cl.add(c);
		}
		boolean findConstructor = false;
		Object instance = null;
		for (Constructor<?> constructor : cl) {
			log.debug("find constructor:" + constructor.getName());
			instance = make(constructor);
			config(instance);
			findConstructor = true;
			break;
		}
		if (findConstructor) {
			return (T) instance;
		} else {
			throw new SysException("cannot make of type:" + beanType.getName());
		}
	}

	@SuppressWarnings("unchecked")
	public <T> T create(Class<?> beanType, String id, String name, String desc) throws SysException {
		log.debug("create by properties: " + "id=" + id + ";name=" + name + ";desc=" + desc);
		BaseInstance bi = new BaseInstance();
		bi.setName(name);
		bi.setId(id);
		bi.setDesc(desc);
		Object instance = newInstance(beanType);
		if (instance != null) {
			bi.setInstance(instance);
			beans.add(bi);
			return (T) bi.getInstance();
		} else {
			throw new SysException("cannot define bean of type:" + beanType.getName());
		}
	}

	@SuppressWarnings("unchecked")
	public <T> T config(Object instance) throws SysException {
		Class<?> beanClass = (Class<?>) instance.getClass();
		Field[] fs = beanClass.getFields();
		Field[] dfs = beanClass.getDeclaredFields();
		List<Field> fl = new ArrayList<>();
		for (Field f : fs) {
			fl.add(f);
		}
		for (Field f : dfs) {
			fl.add(f);
		}
		for (Field field : fl) {
			String fieldName = field.getName();
			Class<?> fieldType = field.getType();
			Annotation[] fieldAnnotations = field.getAnnotations();
			for (Annotation annotation : fieldAnnotations) {
				if (annotation.annotationType() == Auto.class) {
					if (!field.isAccessible()) {
						field.setAccessible(true);
					}
					try {
						field.set(instance, getBeanByType(fieldType));
					} catch (Exception e) {
						e.printStackTrace();
						throw new SysException(e, "can not set field name : " + fieldName);
					}
					break;
				}
				if (annotation.annotationType() == Property.class) {
					Property property = (Property) annotation;
					if (property.value().equals("")) {
						throw new SysException("please use a property");
					}
					if (!field.isAccessible()) {
						field.setAccessible(true);
					}
					try {
						field.set(instance, lookupProperty(property.value()));
					} catch (Exception e) {
						throw new SysException(e, "can not set field name : " + fieldName);
					}
					break;
				}
				if (annotation.annotationType() == Reference.class) {
					Reference ref = (Reference) annotation;
					if (ref.value().equals("")) {
						throw new SysException("please use a reference name on ");
					}
					if (!field.isAccessible()) {
						field.setAccessible(true);
					}
					try {
						field.set(instance, get(ref.value()));
					} catch (Exception e) {
						throw new SysException(e, "can not set field name : " + fieldName);
					}
					break;
				}
				if (annotation.annotationType() == Value.class) {
					Value value = (Value) annotation;
					if (!field.isAccessible()) {
						field.setAccessible(true);
					}
					try {
						field.set(instance, makeValue(value.value()));
					} catch (Exception e) {
						throw new SysException(e, "can not set field name : " + fieldName);
					}
					break;
				}
			}
		}

		Method[] ms = beanClass.getMethods();
		/** Method[] dms = beanClass.getDeclaredMethods(); */
		List<Method> l = new ArrayList<>();
		for (Method m : ms) {
			l.add(m);
		}
		/**
		 * for (Method m : dms) { l.add(m); }
		 */
		List<Method> initMethods = new ArrayList<>();
		for (Method method : l) {
			Annotation[] annotations = method.getAnnotations();
			for (Annotation annotation : annotations) {
				if (annotation.annotationType() == Init.class) {
					initMethods.add(method);
					break;
				}
			}
		}
		if (initMethods.size() > 1) {
			throw new SysException("a class must have only init method :" + beanClass.getName());
		} else {
			for (Method method : initMethods) {
				init(method, instance);
			}
		}
		return (T) instance;
	}

	public void init(Method method, Object instance) throws SysException {
		Parameter[] parameters = method.getParameters();
		List<Object> parameterList = new ArrayList<>();
		for (int i = 0; i < parameters.length; i++) {
			Parameter parameter = parameters[i];
			Class<?> parameterType = parameter.getType();
			String parameterName = parameter.getName();
			Annotation[] parameterAnnotations = parameter.getAnnotations();
			for (Annotation annotation : parameterAnnotations) {
				if (annotation.annotationType() == Auto.class) {
					parameterList.add(this.getBeanByType(parameterType));
					break;
				}
				if (annotation.annotationType() == Property.class) {
					Property property = (Property) annotation;
					String propName = property.value().equals("") ? parameterName : property.value();
					parameterList.add(this.lookupProperty(propName));
					break;
				}
				if (annotation.annotationType() == Reference.class) {
					Reference ref = (Reference) annotation;
					String referenceName = ref.value().equals("") ? parameterName : ref.value();
					parameterList.add(this.get(referenceName));
					break;
				}
				if (annotation.annotationType() == Value.class) {
					Value value = (Value) annotation;
					parameterList.add(makeValue(value.value()));
					break;
				}
				throw new SysException("can not make parameter: " + parameterName);
			}
		}
		try {
			method.invoke(instance, parameterList.toArray());
		} catch (Exception e) {
			e.printStackTrace();
			throw new SysException("can not invoke the method by method of name  :" + method.getName());
		}
	}

	@SuppressWarnings("unchecked")
	public <T> T make(Constructor<?> constructor) throws SysException {
		Parameter[] parameters = constructor.getParameters();
		List<Object> parameterList = new ArrayList<>();
		for (int i = 0; i < parameters.length; i++) {
			Parameter parameter = parameters[i];
			Class<?> parameterType = parameter.getType();
			String parameterName = parameter.getName();
			Annotation[] parameterAnnotations = parameter.getAnnotations();
			for (Annotation annotation : parameterAnnotations) {
				if (annotation.annotationType() == Auto.class) {
					parameterList.add(this.getBeanByType(parameterType));
					break;
				}
				if (annotation.annotationType() == Property.class) {
					Property property = (Property) annotation;
					String propName = property.value().equals("") ? parameterName : property.value();
					parameterList.add(this.lookupProperty(propName));
					break;
				}
				if (annotation.annotationType() == Reference.class) {
					Reference ref = (Reference) annotation;
					String referenceName = ref.value().equals("") ? parameterName : ref.value();
					parameterList.add(this.get(referenceName));
					break;
				}
				if (annotation.annotationType() == Value.class) {
					Value value = (Value) annotation;
					parameterList.add(makeValue(value.value()));
					break;
				}
				throw new SysException("can not make parameter: " + parameterName);
			}
		}
		try {
			return (T) constructor.newInstance(parameterList.toArray());
		} catch (Exception e) {
			throw new SysException("can not instance the instance by constructor:" + constructor.getName());
		}
	}

	public Object lookupProperty(String propName) throws SysException {
		for (String key : properties.keySet()) {
			if (key.equals(propName)) {
				return properties.get(key);
			}
		}
		throw new SysException("not find property:" + propName);
	}

	public Object makeValue(String express) {
		return express;
	}

}
